﻿using System;
using System.IO;
using System.Linq;
using System.Security.Principal;
using System.Xml.Linq;
using KWatkins.NUnitTfs.Tfs2010.TestResultsServiceV1;

namespace KWatkins.NUnitTfs.Tfs2010
{
	internal sealed class Publisher2010 : Publisher
	{
		internal static readonly XNamespace Xmlns = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010";

		internal Publisher2010(Options options)
			: base(options)
		{
		}

		protected override void PublishToTfs(XDocument trx)
		{
			// Uncomment this line to change the runId every time; useful for debugging as you
			// can then use the same test data multiple times.
			//trx.Root.Attribute("id").Value = Guid.NewGuid().ToString("N");

			// Retrieve full details of the build from TFS.
			var buildDetails = GetBuildDetails();

			// Make sure we don't already have a .trc with the same ID uploaded.
			CheckTestRunDoesNotExist(trx);

			// Retrieve the TFS ID of the current user from.
			Guid userId = ReadCurrentUsersIdentity();

			// Create a new test run in TFS.
			var testRun = CreateTestRun(trx, userId, buildDetails);

			// Create a set of test results for the run in TFS.
			var maps = CreateTestResults(trx, userId, testRun.TestRunId);

			// Update test run with an XML of test run details and any associated deployment files.
			UpdateTestRunWithTmiRunAndAssemblies(testRun, trx);

			// Update test results with details of the tests.
			UpdateTestResultsWithDetails(testRun, maps, userId);
		}

		private void UpdateTestResultsWithDetails(TestRun testRun, Map[] maps, Guid userId)
		{
			Console.WriteLine("Updating test results with details...");

			// Add errors from XML into the TestCaseResults and update states.
			foreach (var map in maps)
			{
				map.TestCaseResult.State = (byte)TestResultState.Completed;

				if (Enum.IsDefined(typeof(Outcome), map.UnitTestResultElement.Attribute("outcome").Value))
				{
					Outcome outcome = (Outcome)Enum.Parse(typeof(Outcome), map.UnitTestResultElement.Attribute("outcome").Value);

					map.TestCaseResult.Outcome = (byte)outcome;
				}
				else
				{
					map.TestCaseResult.Outcome = (byte)Outcome.Error;
				}

				map.TestCaseResult.LastUpdatedBy = userId;

				// If we have an error set the message.
				var error = map.UnitTestResultElement.Descendants(Xmlns + "Message").FirstOrDefault();
				if (error != null)
				{
					map.TestCaseResult.ErrorMessage = error.Value;
				}
			}

			// Build an update request for each result. We need to update the state and upload an XML of the details
			// for each test. Again why do we need both? And why a file for EACH test case?
			var resultUpdateRequests = (from map in maps
										select new ResultUpdateRequest
										{
											TestRunId = map.TestCaseResult.Id.TestRunId,
											TestResultId = map.TestCaseResult.Id.TestResultId,
											TestCaseResult = map.TestCaseResult,
											Attachments = new[]
                                                                 {
                                                                     new TestResultAttachment
                                                                     {
                                                                         FileName="tmiResult.tr_",
                                                                         AttachmentType="TmiTestResultDetail",
                                                                         TestRunId = testRun.TestRunId,
                                                                         TestResultId = map.TestCaseResult.Id.TestResultId,
                                                                         Length = map.TestResultDetailAttachmentBytes.Length,
                                                                         ActionPath = "",
                                                                         IsComplete = false,
                                                                         TmiRunId = map.RunId
                                                                     }
                                                                 }

										}).ToArray();

			// Update the test results.
			ResultUpdateResponse[] responses;
			using (var client = new TestResultsServiceSoapClient())
			{
				responses = client.UpdateTestResults(resultUpdateRequests, Options.TeamProject);
			}

			// Now upload an XML of the details for each test.
			Console.WriteLine("Uploading test result details...");
			for (int f = 0; f < resultUpdateRequests.Length; f++)
			{
				FileUploader.UploadFile("tmiResult.tr_", maps[f].TestResultDetailAttachmentBytes, Options.TeamProject, testRun.TestRunId, responses[f].AttachmentIds[0], maps[f].TestCaseResult.Id.TestResultId);
			}
		}

		private void UpdateTestRunWithTmiRunAndAssemblies(TestRun testRun, XDocument trx)
		{
			Console.WriteLine("Updating test run with details...");
			// Update the state of the run.
			testRun.State = (byte)TestRunState.InProgress;
			testRun.PostProcessState = (byte)PostProcessState.Complete;
			testRun.Revision = 1;

			var runId = GetRunId(trx);

			// Create tmiRun.tr_ attachment. Quite why TFS 2010 needs to have webservice calls AND uploads of
			// XML blobs I have no idea...
			var testTmiRunBytes = BuildTestRunTmi(trx).ToBytes();
			var tmiRunAttachment = new TestResultAttachment
			{
				FileName = "tmiRun.tr_",
				AttachmentType = "TmiTestRunSummary",
				TestRunId = testRun.TestRunId,
				Length = testTmiRunBytes.Length,
				ActionPath = "",
				IsComplete = false,
				TmiRunId = runId,
				Comment = string.Format("Test run '{0}' created on '{1:dd/MM/yyyy HH:mm:ss}' and run by '{2}'", GetName(trx), GetStartDate(trx), GetRunUser(trx))
			};


			// Find any files associated with the deployment.
			var deploymentFiles = (from deploymentItem in trx.Descendants(Xmlns + "DeploymentItem")
								   where
									   !string.IsNullOrEmpty(deploymentItem.Attribute("filename").Value)
								   select new FileInfo(deploymentItem.Attribute("filename").Value)).ToArray();
			// Create attachments for them.
			var deploymentFileAttachments = (from file in deploymentFiles
											 select new TestResultAttachment
											 {
												 FileName = file.Name,
												 AttachmentType = "TmiTestRunDeploymentFiles",
												 TestRunId = testRun.TestRunId,
												 Length = file.Length,
												 ActionPath = "",
												 IsComplete = false,
												 TmiRunId = runId,
											 }).ToArray();

			var attachments = (new[] { tmiRunAttachment }).Union(deploymentFileAttachments).ToArray();

			// Update the text run, getting back a set of attachment IDs.
			int[] attachmentIds;
			using (var client = new TestResultsServiceSoapClient())
			{
				client.UpdateTestRun(out attachmentIds, testRun, attachments, null, Options.TeamProject);
			}

			// Upload tmiRun.tr_.
			Console.WriteLine("Uploading test run details...");
			FileUploader.UploadFile("tmiRun.tr_", testTmiRunBytes, Options.TeamProject, testRun.TestRunId, attachmentIds[0]);

			// Upload deployment files.
			if (deploymentFiles.Length > 0)
			{
				Console.WriteLine("Uploading assemblies...");
				for (int f = 0; f < deploymentFiles.Length; f++)
				{
					FileUploader.UploadFile(deploymentFiles[f].Name, File.ReadAllBytes(deploymentFiles[f].FullName), Options.TeamProject, testRun.TestRunId, attachmentIds[f + 1]);
				}
			}
		}

		private static XDocument BuildTestRunTmi(XDocument trx)
		{
			// TestRunTmi is a stripped down .trx.
			var testRunTmi = new XDocument(trx);

			testRunTmi.Descendants(Xmlns + "TestDefinitions").Remove();
			testRunTmi.Descendants(Xmlns + "Results").Remove();
			return testRunTmi;
		}

		private Map[] CreateTestResults(XDocument trx, Guid userId, int testRunId)
		{
			Console.WriteLine("Creating test results...");
			var runId = GetRunId(trx);

			// Build a list of TestCaseResult instances from the <UnitTest> and <UnitTestResult> elements in the .trx. We use
			// this approach (rather than a pure object approach) so any .trx can be used - users could use any XSLT they like,
			// even for another framework, and this would still work. Guess we could parse the .trx and just use objects but
			// that seems like overkill.
			int testResultId = 100000;

			var testCaseResults = (from unitTest in trx.Root.Descendants(Xmlns + "UnitTest")
								   join unitTestResult in trx.Root.Descendants(Xmlns + "UnitTestResult") on unitTest.Attribute("id").Value equals unitTestResult.Attribute("testId").Value
								   select new Map
								   {
									   RunId = runId,
									   UnitTestElement = unitTest,
									   UnitTestResultElement = unitTestResult,
									   TestCaseResult = new TestCaseResult
									   {
										   ResolutionStateId = -1,
										   Owner = userId,
										   RunBy = userId,
										   TestCaseTitle = unitTestResult.Attribute("testName").Value,
										   AutomatedTestName = unitTestResult.Attribute("testName").Value,
										   AutomatedTestStorage = BuildAutomatedTestStorage(unitTest.Attribute("storage").Value),
										   AutomatedTestType = "Unit Test",
										   AutomatedTestTypeId = unitTestResult.Attribute("testType").Value,
										   AutomatedTestId = Guid.NewGuid().ToString("D"),
										   Id = new TestCaseResultIdentifier
										   {
											   TestRunId = testRunId,
											   TestResultId = testResultId++
										   }
									   }
								   }).ToArray();


			using (var client = new TestResultsServiceSoapClient())
			{
				client.CreateTestResults(testCaseResults.Select(r => r.TestCaseResult).ToArray(), Options.TeamProject);
			}

			return testCaseResults;
		}

		private static string BuildAutomatedTestStorage(string storage)
		{
			return storage.Substring(storage.LastIndexOf('\\') + 1).ToLower();
		}

		private TestRun CreateTestRun(XDocument trx, Guid userId, BuildServiceV3.BuildDetail buildDetails)
		{
			Console.WriteLine("Creating test run...");
			using (var client = new TestResultsServiceSoapClient())
			{
				var testRun = new TestRun
				{
					Title = GetName(trx),
					Owner = userId,
					State = (byte)TestRunState.NotStarted,
					BuildUri = buildDetails.Uri,
					BuildNumber = Options.Build,
					BuildPlatform = Options.Platform,
					BuildFlavor = Options.Flavour,
					// TODO: Get correct times into <Times> element and use those instead.
					StartDate = GetStartDate(trx),
					CompleteDate = GetStartDate(trx).AddMinutes(1),
					PostProcessState = (byte)PostProcessState.Complete,
					Iteration = Options.TeamProject,
					Version = 1000,
					TeamProject = Options.TeamProject
				};
				var result = client.CreateTestRun(testRun, Options.TeamProject);

				testRun.TestRunId = result.TestRunId;

				return testRun;
			}
		}

		private static Guid ReadCurrentUsersIdentity()
		{
			Console.WriteLine("Reading users identity...");
			using (var client = new IdentityManagementServiceV3.IdentityManagementWebServiceSoapClient())
			{
				var currentUser = WindowsIdentity.GetCurrent();

				if (currentUser == null)
				{
					throw new InvalidOperationException("Could not find current Windows user.");
				}

				var result = client.ReadIdentities(0, new[] { currentUser.Name }, 0, 0);

				if ((result == null) || (result.Length != 1) || (result[0].Length != 1))
				{
					throw new InvalidOperationException(string.Format("Could not find user {0} in TFS.", currentUser.Name));
				}

				return result[0][0].TeamFoundationId;
			}
		}

		private static void CheckTestRunDoesNotExist(XDocument trx)
		{
			Console.WriteLine("Checking test run does not exist...");
			using (var client = new TestResultsServiceSoapClient())
			{
				var result = client.QueryTestRunByTmiRunId(GetRunId(trx));

				if (result != null)
				{
					throw new InvalidOperationException("Test run already uploaded.");
				}
			}
		}

		private BuildServiceV3.BuildDetail GetBuildDetails()
		{
			using (var client = new BuildServiceV3.BuildWebServiceSoapClient())
			{
				var specs = new[]
                            {
                                new BuildServiceV3.BuildDetailSpec
                                {
                                    DefinitionPath = string.Format(@"\{0}\*", Options.TeamProject),
                                    BuildNumber = Options.Build,
                                    Status = BuildServiceV3.BuildStatus.All,
                                    Item = new BuildServiceV3.BuildDefinitionSpec
                                           {
                                               FullPath = string.Format(@"\{0}\*", Options.TeamProject)
                                           },
                                    QueryOptions = BuildServiceV3.QueryOptions.None
                                }
                            };
				var results = client.QueryBuilds(specs);

				if ((results == null) || (results.Length != 1) || (results[0].Builds.Length != 1))
				{
					throw new InvalidOperationException(string.Format("Could not find details of build \"{0}\".", Options.Build));
				}
				return results[0].Builds[0];
			}
		}

		private static string GetName(XDocument trx)
		{
			return trx.Root.Attribute("name").Value;
		}

		private static DateTime GetStartDate(XDocument trx)
		{
			var name = GetName(trx);
			return DateTime.ParseExact(name.Substring(name.IndexOf(' ') + 1), "yyyy-MM-dd HH:mm:ss", null);
		}

		private static string GetRunUser(XDocument trx)
		{
			return trx.Root.Attribute("runUser").Value;
		}
	}
}
